package hydrap2p.manager;

import hydrap2p.ServiceLocation;
import hydrap2p.helper.Utility;
import hydrap2p.modulemanager.ModuleControl;
import hydrap2p.modulemanager.ModuleInfo;

import java.io.File;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.Map.Entry;

import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;
import org.dtools.ini.AdvancedIniFile;
import org.dtools.ini.AdvancedIniSection;
import org.dtools.ini.IniFile;
import org.dtools.ini.IniFileReader;
import org.dtools.ini.IniFileWriter;
import org.dtools.ini.IniItem;
import org.dtools.ini.IniSection;
import org.jvnet.winp.WinProcess;

public class ModuleManagerImpl {
	private Map<String,ModuleConfig> sharedconf;
	private Map<String,ModuleConfig> userconf;
	private Map<String,ModuleConfig> activeconf;
	private Map<String,ModuleStatus> status;
	
	private Tree<String> dependencies;
	
	private String sharedConfDir;
	private String userConfDir;
	private ManagerImpl manager;
	
	private class ModuleConfig{
		String name;
		String command;
		boolean autostart;
		File where;
		String control;
		
		public List<String> needs;		// the modules that it needs
		
		public ModuleConfig()
		{
			needs=new LinkedList<String>();
		}
		
		@Override
		public boolean equals(Object o)
		{
			if (! (o instanceof ModuleConfig))
				return false;
			ModuleConfig other=(ModuleConfig)o;
			return name.equals(other.name) &&
				   command.equals(other.command) &&
				   where.equals(other.where) &&
				   autostart==other.autostart &&
				   control.equals(other.control);
		}
		
	}
	
	private class ModuleStatus {
		ModuleConfig conf;
		
		boolean isrunning;
		
		Process child;
		StreamCopier outcopier;		// thread copying from stdout to my stdout
		StreamCopier errcopier;		// thread copying from stderr to my stderr

		@Override
		public String toString()
		{
			return "Module status (name:"+conf.name+" running:"+isrunning;
		}
	}
	
	private class Shutdown extends Thread
	{
		@Override
		public void run()
		{
			stopAllModules();
		}
	}
	
	public ModuleManagerImpl(ManagerImpl manager)
	{
		sharedConfDir=Utility.appendPath(Utility.getSharedConfigurationPath(),"modules");
		userConfDir=Utility.appendPath(Utility.getUserConfigurationPath(),"modules");
		status=new TreeMap<String,ModuleStatus>();
		dependencies=new Tree<String>("Manager");
		activeconf=new TreeMap<String,ModuleConfig>();
		this.manager=manager;
		load();

		Runtime.getRuntime().addShutdownHook(new Shutdown());
	}
	
	public void load()
	{
		sharedconf=loadConfDir(new File(sharedConfDir));
		userconf=loadConfDir(new File(userConfDir));
		
		// Combine the configurations, with the user configuration overwriting the shared configuration
		activeconf.putAll(sharedconf);
		activeconf.putAll(userconf);
		
		parseDependencyGraph();
	}
	
	public Map<String,ModuleConfig> loadConfDir(File confdir)
	{
		Map<String,ModuleConfig> conf=new TreeMap<String,ModuleConfig>();
		if (!confdir.isDirectory())
			return conf;
		for (File conffile:confdir.listFiles())
		{
			if (conffile.isDirectory())
				continue;
			if (!conffile.getPath().toLowerCase().endsWith(".ini"))
				continue;
			
			IniFile reader = new AdvancedIniFile();
			try {
				new IniFileReader(reader,conffile).read();
			} catch (IOException e) {
				// Could not read the file
				continue;
			}
			
			for (IniSection section:reader.getSections())
			{
				if (conf.containsKey(section.getName()))
				{
					System.err.println("Warning: Duplicate module definition for "+section.getName()+" found in "+conffile.getAbsolutePath());
					continue;
				}
				
				ModuleConfig temp=new ModuleConfig();
				temp.where=conffile;
				temp.name=section.getName();
				if (section.getItem("autostart")!=null)
					temp.autostart=Boolean.parseBoolean(section.getItem("autostart").getValue());
				if (section.getItem("control")!=null)
					temp.control=section.getItem("control").getValue();
				
				if (section.getItem("command")!=null)
				{
					IniItem temp2=section.getItem("command");
					temp.command=temp2.getValue();
					if (!temp2.getEndLineComment().isEmpty())
						temp.command=temp.command+IniItem.COMMENT_SYMBOL+temp2.getEndLineComment();
					temp.command=expandModuleCommand(temp.command);
				}
				if (temp.name!=null && temp.command!=null)
				{
					conf.put(section.getName(), temp);
					
					if (section.getItem("depends")!=null)
					{
						String strdependencies = section.getItem("depends").getValue();
						String[] dependencies=strdependencies.split(",");
						for (String dependency : dependencies)
							temp.needs.add(dependency.trim());
					}
				}
				
			}
		}	
		return conf;
	}
	
	private static String expandModuleCommand(String command)
	{
		String temp=command;
		
		// automatically expand lib/*
		File directory=new File("lib");
		if (directory.exists())
		{
			StringBuilder filelist=new StringBuilder();
			String[] files=directory.list();
			for (String file:files)
			{
				if (file.toLowerCase().endsWith(".jar"))
					filelist.append(";lib/"+file);
			}
			temp=temp.replaceAll(";lib/\\*", filelist.toString());
		}
		else
		{
			System.err.println("Could not find directory "+directory.getAbsolutePath());
		}
		return temp;
	}
	
	private void parseDependencyGraph()
	{
		List<Tree<String>> newleaves=new LinkedList<Tree<String>>();
		newleaves.add(dependencies);
		while (newleaves.size()>0)
		{
			Tree<String> current=newleaves.remove(0);
			String payload=current.getPayload();
			for (ModuleConfig conf:activeconf.values())
			{
				if (conf.needs.contains(payload))
				{
					Tree<String> leaf=new Tree<String>(conf.name);
					newleaves.add(leaf);
					current.addChild(leaf);
				}
			}
		}
	}
	
	private void updateUserConf(ModuleConfig conf)
	{
		File where=conf.where;
		IniFile output=new AdvancedIniFile();

		if (where==null){
			where=new File(Utility.appendPath(userConfDir, conf.name+".ini"));
		}
		where.getParentFile().mkdirs();
		if (where.isFile())
		{
			try {
				new IniFileReader(output,where).read();
			} catch (IOException e) {
				// TODO give some logging about why the conf didn't get loaded
			}
		}
		
		IniSection section=output.getSection(conf.name);
		if (section==null)
			section=new AdvancedIniSection(conf.name);
		IniItem temp;
		temp=section.getItem("autostart");
		if (temp==null)
			temp=new IniItem("autostart");
		temp.setValue(conf.autostart);
		section.removeItem("autostart");
		section.addItem(temp);
				
		temp=section.getItem("command");
		if (temp==null)
			temp=new IniItem("command");
		temp.setValue(conf.command);
		section.removeItem("command");
		section.addItem(temp);
		
		temp=section.getItem("control");
		if (temp==null)
			temp=new IniItem("control");
		temp.setValue(conf.control);
		section.removeItem("control");
		section.addItem(temp);
		
		temp=section.getItem("depends");
		if (temp==null)
		{
			temp=new IniItem("depends");
			section.addItem(temp);
		}
		
		try {
			output.addSection(section);
			new IniFileWriter(output,where).write();
		} catch (IOException e) {
			// TODO give some logging about why the conf didn't get saved
		}
	}
	
	public Set<String> getModuleNames()
	{
		load();
		return activeconf.keySet();
	}
	
	public ModuleInfo getModuleInfo(String name)
	{
		ModuleInfo test=new ModuleInfo();
		ModuleConfig config=activeconf.get(name);
		if (config!=null)
		{
			test.autostart=config.autostart;
			test.name=name;
			test.running=isRunning(name);
			return test;
		}
		else
			return null;
	}
	
	public boolean isAutostart(String name)
	{
		if (activeconf.containsKey(name))
			return activeconf.get(name).autostart;
		else
			System.err.println("Module named "+name+" is not managed");
		return false;
	}
	public boolean isRunning(String name){
		if (status.containsKey(name))
			return status.get(name).isrunning;
		//else
		//	System.err.println("Module named "+name+" has never been started before");
		return false;
	}
	public void setAutostart(String name, boolean autostart)
	{
		if (!activeconf.containsKey(name))
			return;
		
		if (!userconf.containsKey(name))
		{
			ModuleConfig temp=activeconf.get(name);
			temp.autostart=autostart;
			temp.where=null;
			userconf.put(name, temp);
		}
		else
		{
			ModuleConfig temp=userconf.get(name);
			temp.autostart=autostart;
			userconf.put(name, temp);
		}
		updateUserConf(userconf.get(name));
	}

	/**
	 * If a service starts somewhere else, set the flag to say that it's running
	 * @param service
	 */
	public void serviceStarted(String service)
	{
		for (Entry<String, ModuleConfig> entry : activeconf.entrySet())
		{
			if (entry.getValue().control.equals(service))
			{
				ModuleStatus mine=status.get(entry.getKey());
				if (mine==null || !mine.isrunning)
				{
					mine=new ModuleStatus();
					mine.conf=entry.getValue();
					mine.isrunning=true;
					status.put(entry.getKey(), mine);
				}
			}
		}
	}

	/**
	 * If a service that was started elsewhere stops, take note
	 * @param service
	 */
	public void serviceStopped(String service)
	{
		for (Entry<String, ModuleConfig> entry : activeconf.entrySet())
		{
			if (entry.getValue().control.equals(service))
			{
				ModuleStatus mine=status.get(entry.getKey());
				if (mine!=null && mine.isrunning==true && mine.child==null)		// if we didn't start this module, then believe that it's not running
				{
					mine.isrunning=false;
					status.remove(entry.getKey());
				}
			}
		}
	}
	
	public void startModule(String name)
	{
		if (isRunning(name))
			return;
		
		Process child;
		if (activeconf.containsKey(name))
			try {
				for (String parent : activeconf.get(name).needs)
				{
					if (!parent.equals("Manager") && !isRunning(parent))
						startModule(parent);
				}
				
				ModuleStatus mine;
				mine=status.get(name);
				if (mine==null)
				{
					mine=new ModuleStatus();
					mine.conf=activeconf.get(name);
				}
				System.out.println("Starting module "+mine.conf.name);
				child=Runtime.getRuntime().exec(mine.conf.command);
				mine.child=child;
				mine.outcopier=new StreamCopier(this,name,child.getInputStream(),System.out);
				mine.outcopier.start();
				mine.errcopier=new StreamCopier(this,name,child.getErrorStream(),System.err);
				mine.errcopier.start();
				
				try {
					int ret=child.exitValue();
					// exitValue will Exception if it's still running
					// if not, it quit
					System.err.println("Failed to start module "+mine.conf.name);
					return;
				} catch (IllegalThreadStateException e)
				{}
				
				mine.isrunning=true;
				status.put(name, mine);
				
				System.out.println("Started module "+mine.conf.name);
				
			} catch (IOException e) {
			}
	}
	public void stopModule(String name)
	{
		ModuleStatus modulestatus=status.get(name);
		if (modulestatus!=null && modulestatus.isrunning)
		{
			Tree<String> node=dependencies.getChildByPayloadRecursive(name);
			if (node!=null)
			{
				for (Tree<String> child : node.getChildren())
				{
					if (isRunning(child.getPayload()))
						stopModule(child.getPayload());
				}
			}
			
			System.out.println("Stopping module: "+name);
			try {
				ServiceLocation there=manager.getServiceLocation(status.get(name).conf.control);
				
				if (there!=null)
				{
					TTransport transport=new TSocket(there.address,there.port);
					transport.open();
					ModuleControl.Client connection=new ModuleControl.Client(new TBinaryProtocol(transport));
					System.out.println("Stopped "+name+" through Thrift");
					connection.stop();
				}
				else
				{
//					System.out.println("Could not find ModuleControl instance for "+status.get(name).conf.control);
				}
			} catch (Exception e) {
//				e.printStackTrace();
			}
			if (modulestatus.child!=null)
			{
				if (modulestatus.outcopier!=null)
					modulestatus.outcopier.shutdown();
				if (modulestatus.errcopier!=null)
					modulestatus.errcopier.shutdown();
				
				if (System.getProperty("os.name").toLowerCase().contains("win"))
					new WinProcess(modulestatus.child).killRecursively();
				modulestatus.child.destroy();
			}
			modulestatus.isrunning=false;
		}
	}
	public void stopAllModules()
	{
		for (String cur:status.keySet())
			stopModule(cur);
			
	}
}
